Ch. 2: Extending our functional test using the unittest module

Using a functional test to scope out a miniumum viable app

We'll use selenium to simulate a user visiting our website in a real web browser. We call our tests with selenium functional tests because they let us see how the app functions from the user's point of view.

Functional tests tend to track what we might call the User Story, i.e. how a user might work with a particular feature and how the app should respond to them.

Functional Test == Acceptance Test == End-to-End Test

Minimum viable app

What is the simplest thing that we can build that is still useful?


In [5]:
#%cd ../examples/superlists/
%ls


db.sqlite3  functional_tests.py  manage.py*  superlists/

In [8]:
%%writefile functional_tests.py

from selenium import webdriver

browser = webdriver.Firefox()

# Edith has heard about a cool new online to-do app. She goes
# to check out its homepage
browser.get('http://localhost:8000')

# She notices the page title and header mention to-do lists
assert 'To-Do' in browser.title

# She is invited to enter a to-do item straight away

# She types "Buy peacock feathers" into a text box (Edith's hobby
# is tying fly-fishing lures)

# When she hits enter, the page updates, and now the page lists
# "1: Buy peacock feathers" as an item in a to-do list

# There is still a text box inviting her to add another item. She
# enters "Use peacock feathers to make a fly" (Edith is very methodical)

# The page updates again, and now shows both items on her list

# Edith wonders whether the site will remember her list. Then she sees
# that the site has generated a unique URL for her -- there is some
# explanatory text to that effect.

# She visits that URL - her to-do list is still there.

# Satisfied, she goes back to sleep

browser.quit()


Overwriting functional_tests.py

Notice that I've updated the assert to include the word "To-Do" instead of "Django". Now our test should fail. Let's check that it fails.


In [9]:
# First start up the server:
#!python3 manage.py runserver

# Run test
!python3 functional_tests.py


Traceback (most recent call last):
  File "functional_tests.py", line 11, in <module>
    assert 'To-Do' in browser.title
AssertionError

We got what was called an expected fail which is what we wanted!

Python Standard Library's unittest Module

There are a couple of little annoyances we should probably deal with. Firstly, the message "AssertionError" isn’t very helpful—it would be nice if the test told us what it actually found as the browser title. Also, it’s left a Firefox window hanging around the desktop, it would be nice if this would clear up for us automatically.

One option would be to use the second parameter to the assert keyword, something like:

assert 'To-Do' in browser.title, "Browser title was " + browser.title

And we could also use a try/finally to clean up the old Firefox window. But these sorts of problems are quite common in testing, and there are some ready-made solutions for us in the standard library’s unittest module. Let’s use that! In functional_tests.py:


In [14]:
%%writefile functional_tests.py

from selenium import webdriver
import unittest

class NewVisitorTest(unittest.TestCase):  #1

    def setUp(self):  #2
        self.browser = webdriver.Firefox()
        self.browser.implicitly_wait(3) # Wait three seconds before trying anything.

    def tearDown(self):  #3
        self.browser.quit()

    def test_can_start_a_list_and_retrieve_it_later(self):  #4
        # Edith has heard about a cool new online to-do app. She goes
        # to check out its homepage
        self.browser.get('http://localhost:8000')

        # She notices the page title and header mention to-do lists
        self.assertIn('To-Do', self.browser.title)  #5
        self.fail('Finish the test!')  #6

        # She is invited to enter a to-do item straight away
        # [...rest of comments as before]

if __name__ == '__main__':  #7
    unittest.main(warnings='ignore')  #8


Overwriting functional_tests.py

Some things to notice about our new test file:

  1. Tests are organised into classes, which inherit from unittest.TestCase.

  2. and

  3. setUp and tearDown are special methods which get run before and after each test. I’m using them to start and stop our browser—note that they’re a bit like a try/except, in that tearDown will run even if there’s an error during the test itself.[4] No more Firefox windows left lying around!

  4. The main body of the test is in a method called test_can_start_a_list_and_retrieve_itlater. Any method whose name starts with test is a test method, and will be run by the test runner. You can have more than one test method per class. Nice descriptive names for our test methods are a good idea too.

  5. We use self.assertIn instead of just assert to make our test assertions. unittest provides lots of helper functions like this to make test assertions, like assertEqual, assertTrue, assertFalse, and so on. You can find more in the unittest documentation.

  6. self.fail just fails no matter what, producing the error message given. I’m using it as a reminder to finish the test.

  7. Finally, we have the if name == 'main' clause (if you’ve not seen it before, that’s how a Python script checks if it’s been executed from the command line, rather than just imported by another script). We call unittest.main(), which launches the unittest test runner, which will automatically find test classes and methods in the file and run them.

  8. warnings='ignore' suppresses a superfluous ResourceWarning which was being emitted at the time of writing. It may have disappeared by the time you read this; feel free to try removing it!

Running our new test


In [13]:
!python3 functional_tests.py


F
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "functional_tests.py", line 19, in test_can_start_a_list_and_retrieve_it_later
    self.assertIn('To-Do', self.browser.title)  #5
AssertionError: 'To-Do' not found in 'Welcome to Django'

----------------------------------------------------------------------
Ran 1 test in 2.334s

FAILED (failures=1)

We got the same expected failure but now it looks nice!